1. Perishable Goods Network

Perishable Goods Network

Example business network that shows growers, shippers and importers defining contracts for the price of perishable goods, based on temperature readings received for shipping containers.

Participants Grower Importer Shipper

Assets Contract Shipment

Transactions TemperatureReading ShipmentReceived SetupDemo

2. 模型

perishable.cto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/**
* A business network for shipping perishable goods
* The cargo is temperature controlled and contracts
* can be negociated based on the temperature
* readings received for the cargo
*/

namespace org.acme.shipping.perishable

/**
* The type of perishable product being shipped
*/
enum ProductType {
o BANANAS
o APPLES
o PEARS
o PEACHES
o COFFEE
}

/**
* The status of a shipment
*/
enum ShipmentStatus {
o CREATED
o IN_TRANSIT
o ARRIVED
}

/**
* An abstract transaction that is related to a Shipment
*/
abstract transaction ShipmentTransaction {
--> Shipment shipment
}

/**
* An temperature reading for a shipment. E.g. received from a
* device within a temperature controlled shipping container
*/
transaction TemperatureReading extends ShipmentTransaction {
o Double centigrade
}

/**
* A notification that a shipment has been received by the
* importer and that funds should be transferred from the importer
* to the grower to pay for the shipment.
*/
transaction ShipmentReceived extends ShipmentTransaction {
}

/**
* A shipment being tracked as an asset on the ledger
*/
asset Shipment identified by shipmentId {
o String shipmentId
o ProductType type
o ShipmentStatus status
o Long unitCount
o TemperatureReading[] temperatureReadings optional
--> Contract contract
}

/**
* Defines a contract between a Grower and an Importer to ship using
* a Shipper, paying a set unit price. The unit price is multiplied by
* a penality factor proportional to the deviation from the min and max
* negociated temperatures for the shipment.
*/
asset Contract identified by contractId {
o String contractId
--> Grower grower
--> Shipper shipper
--> Importer importer
o DateTime arrivalDateTime
o Double unitPrice
o Double minTemperature
o Double maxTemperature
o Double minPenaltyFactor
o Double maxPenaltyFactor
}

/**
* A concept for a simple street address
*/
concept Address {
o String city optional
o String country
o String street optional
o String zip optional
}

/**
* An abstract participant type in this business network
*/
abstract participant Business identified by email {
o String email
o Address address
o Double accountBalance
}

/**
* A Grower is a type of participant in the network
*/
participant Grower extends Business {
}

/**
* A Shipper is a type of participant in the network
*/
participant Shipper extends Business {
}

/**
* An Importer is a type of participant in the network
*/
participant Importer extends Business {
}

/**
* JUST FOR INITIALIZING A DEMO
*/
transaction SetupDemo {
}

3. 模板构建

建立SetupDemo交易:

This transaction populates the Participant Registries with a Grower, an Importer and a Shipper. The Asset Registries will have a Contract asset and a Shipment asset.

1
2
3
{
"$class": "org.acme.shipping.perishable.SetupDemo"
}

建立TemperatureReading交易:

If the temperature reading falls outside the min/max range of the contract, the price received by the grower will be reduced. You may submit several readings if you wish. Each reading will be aggregated within SHIP_001 Shipment Asset Registry.

1
2
3
4
5
{
"$class": "org.acme.shipping.perishable.TemperatureReading",
"centigrade": 8,
"shipment": "resource:org.acme.shipping.perishable.Shipment#SHIP_001"
}

建立ShipmentReceived交易:

Submit a ShipmentReceived transaction for SHIP_001 to trigger the payout to the grower, based on the parameters of the CON_001 contract.

If the date-time of the ShipmentReceived transaction is after the arrivalDateTime on CON_001 then the grower will no receive any payment for the shipment.

1
2
3
4
{
"$class": "org.acme.shipping.perishable.ShipmentReceived",
"shipment": "resource:org.acme.shipping.perishable.Shipment#SHIP_001"
}

4. perishable.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
'use strict';

const AdminConnection = require('composer-admin').AdminConnection;
const BusinessNetworkConnection = require('composer-client').BusinessNetworkConnection;
const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition;
const IdCard = require('composer-common').IdCard;
const MemoryCardStore = require('composer-common').MemoryCardStore;
const path = require('path');

require('chai').should();
let sinon = require('sinon');

const namespace = 'org.acme.shipping.perishable';
let grower_id = 'farmer@email.com';
let importer_id = 'supermarket@email.com';

describe('Perishable Shipping Network', () => {
// In-memory card store for testing so cards are not persisted to the file system
const cardStore = new MemoryCardStore();
let adminConnection;
let businessNetworkConnection;
let factory;
let clock;

before(() => {
// Embedded connection used for local testing
const connectionProfile = {
name: 'embedded',
type: 'embedded'
};
// Embedded connection does not need real credentials
const credentials = {
certificate: 'FAKE CERTIFICATE',
privateKey: 'FAKE PRIVATE KEY'
};

// PeerAdmin identity used with the admin connection to deploy business networks
const deployerMetadata = {
version: 1,
userName: 'PeerAdmin',
roles: [ 'PeerAdmin', 'ChannelAdmin' ]
};
const deployerCard = new IdCard(deployerMetadata, connectionProfile);
deployerCard.setCredentials(credentials);

const deployerCardName = 'PeerAdmin';
adminConnection = new AdminConnection({ cardStore: cardStore });

const adminUserName = 'admin';
let adminCardName;
let businessNetworkDefinition;

return adminConnection.importCard(deployerCardName, deployerCard).then(() => {
return adminConnection.connect(deployerCardName);
}).then(() => {
businessNetworkConnection = new BusinessNetworkConnection({ cardStore: cardStore });

return BusinessNetworkDefinition.fromDirectory(path.resolve(__dirname, '..'));
}).then(definition => {
businessNetworkDefinition = definition;
// Install the Composer runtime for the new business network
return adminConnection.install(businessNetworkDefinition.getName());
}).then(() => {
// Start the business network and configure an network admin identity
const startOptions = {
networkAdmins: [
{
userName: adminUserName,
enrollmentSecret: 'adminpw'
}
]
};
return adminConnection.start(businessNetworkDefinition, startOptions);
}).then(adminCards => {
// Import the network admin identity for us to use
adminCardName = `${adminUserName}@${businessNetworkDefinition.getName()}`;
return adminConnection.importCard(adminCardName, adminCards.get(adminUserName));
}).then(() => {
// Connect to the business network using the network admin identity
return businessNetworkConnection.connect(adminCardName);
}).then(() => {
factory = businessNetworkConnection.getBusinessNetwork().getFactory();
const setupDemo = factory.newTransaction(namespace, 'SetupDemo');
return businessNetworkConnection.submitTransaction(setupDemo);
});
});

beforeEach(() => {
clock = sinon.useFakeTimers();
});

afterEach(function () {
clock.restore();
});

describe('#shipment', () => {

it('should receive base price for a shipment within temperature range', () => {
// submit the temperature reading
^^^
});

it('should receive nothing for a late shipment', () => {
// submit the temperature reading
^^^
});

it('should apply penalty for min temperature violation', () => {
^^^
});

it('should apply penalty for max temperature violation', () => {
// submit the temperature reading
^^^
});
});
});

receive base price

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
it('should receive base price for a shipment within temperature range', () => {
// submit the temperature reading
const tempReading = factory.newTransaction(namespace, 'TemperatureReading');
tempReading.shipment = factory.newRelationship(namespace, 'Shipment', 'SHIP_001');
tempReading.centigrade = 4.5;
return businessNetworkConnection.submitTransaction(tempReading)
.then(() => {
// submit the shipment received
const received = factory.newTransaction(namespace, 'ShipmentReceived');
received.shipment = factory.newRelationship(namespace, 'Shipment', 'SHIP_001');
return businessNetworkConnection.submitTransaction(received);
})
.then(() => {
return businessNetworkConnection.getParticipantRegistry(namespace + '.Grower');
})
.then((growerRegistry) => {
// check the grower's balance
return growerRegistry.get(grower_id);
})
.then((newGrower) => {
// console.log(JSON.stringify(businessNetworkConnection.getBusinessNetwork().getSerializer().toJSON(newGrower)));
newGrower.accountBalance.should.equal(2500);
})
.then(() => {
return businessNetworkConnection.getParticipantRegistry(namespace + '.Importer');
})
.then((importerRegistry) => {
// check the importer's balance
return importerRegistry.get(importer_id);
})
.then((newImporter) => {
newImporter.accountBalance.should.equal(-2500);
})
.then(() => {
return businessNetworkConnection.getAssetRegistry(namespace + '.Shipment');
})
.then((shipmentRegistry) => {
// check the state of the shipment
return shipmentRegistry.get('SHIP_001');
})
.then((shipment) => {
shipment.status.should.equal('ARRIVED');
});
});

receive nothing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
it('should receive nothing for a late shipment', () => {
// submit the temperature reading
const tempReading = factory.newTransaction(namespace, 'TemperatureReading');
tempReading.shipment = factory.newRelationship(namespace, 'Shipment', 'SHIP_001');
tempReading.centigrade = 4.5;
// advance the javascript clock to create a time-advanced test timestamp
clock.tick(1000000000000000);
return businessNetworkConnection.submitTransaction(tempReading)
.then(() => {
// submit the shipment received
const received = factory.newTransaction(namespace, 'ShipmentReceived');
received.shipment = factory.newRelationship(namespace, 'Shipment', 'SHIP_001');
return businessNetworkConnection.submitTransaction(received);
})
.then(() => {
return businessNetworkConnection.getParticipantRegistry(namespace + '.Grower');
})
.then((growerRegistry) => {
// check the grower's balance
return growerRegistry.get(grower_id);
})
.then((newGrower) => {
// console.log(JSON.stringify(businessNetworkConnection.getBusinessNetwork().getSerializer().toJSON(newGrower)));
newGrower.accountBalance.should.equal(2500);
})
.then(() => {
return businessNetworkConnection.getParticipantRegistry(namespace + '.Importer');
})
.then((importerRegistry) => {
// check the importer's balance
return importerRegistry.get(importer_id);
})
.then((newImporter) => {
newImporter.accountBalance.should.equal(-2500);
});
});

apply penalty for min temperature violation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
it('should apply penalty for min temperature violation', () => {
// submit the temperature reading
const tempReading = factory.newTransaction(namespace, 'TemperatureReading');
tempReading.shipment = factory.newRelationship(namespace, 'Shipment', 'SHIP_001');
tempReading.centigrade = 1;
return businessNetworkConnection.submitTransaction(tempReading)
.then(() => {
// submit the shipment received
const received = factory.newTransaction(namespace, 'ShipmentReceived');
received.shipment = factory.newRelationship(namespace, 'Shipment', 'SHIP_001');
return businessNetworkConnection.submitTransaction(received);
})
.then(() => {
return businessNetworkConnection.getParticipantRegistry(namespace + '.Grower');
})
.then((growerRegistry) => {
// check the grower's balance
return growerRegistry.get(grower_id);
})
.then((newGrower) => {
// console.log(JSON.stringify(businessNetworkConnection.getBusinessNetwork().getSerializer().toJSON(newGrower)));
newGrower.accountBalance.should.equal(4000);
})
.then(() => {
return businessNetworkConnection.getParticipantRegistry(namespace + '.Importer');
})
.then((importerRegistry) => {
// check the importer's balance
return importerRegistry.get(importer_id);
})
.then((newImporter) => {
newImporter.accountBalance.should.equal(-4000);
});
});

apply penalty for max temperature violation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
it('should apply penalty for max temperature violation', () => {
// submit the temperature reading
const tempReading = factory.newTransaction(namespace, 'TemperatureReading');
tempReading.shipment = factory.newRelationship(namespace, 'Shipment', 'SHIP_001');
tempReading.centigrade = 11;
return businessNetworkConnection.submitTransaction(tempReading)
.then(() => {
// submit the shipment received
const received = factory.newTransaction(namespace, 'ShipmentReceived');
received.shipment = factory.newRelationship(namespace, 'Shipment', 'SHIP_001');
return businessNetworkConnection.submitTransaction(received);
})
.then(() => {
return businessNetworkConnection.getParticipantRegistry(namespace + '.Grower');
})
.then((growerRegistry) => {
// check the grower's balance
return growerRegistry.get(grower_id);
})
.then((newGrower) => {
// console.log(JSON.stringify(businessNetworkConnection.getBusinessNetwork().getSerializer().toJSON(newGrower)));
newGrower.accountBalance.should.equal(5000);
})
.then(() => {
return businessNetworkConnection.getParticipantRegistry(namespace + '.Importer');
})
.then((importerRegistry) => {
// check the importer's balance
return importerRegistry.get(importer_id);
})
.then((newImporter) => {
newImporter.accountBalance.should.equal(-5000);
});
});

5. 测试结果

ship1

ship2